

package com.szmmsoft.mp.common.interceptor;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.ttl.TransmittableThreadLocal;

import com.szmmsoft.mp.common.anno.Log;
import com.szmmsoft.mp.common.constant.Constants;
import com.szmmsoft.mp.common.enums.IncludeEnum;
import com.szmmsoft.mp.common.model.LogRecord;
import com.szmmsoft.mp.common.model.LogResponse;
import com.szmmsoft.mp.common.model.RecordableServletHttpRequest;
import com.szmmsoft.mp.common.model.RecordableServletHttpResponse;
import com.szmmsoft.mp.common.service.LogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Tag;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Clock;
import java.util.Set;

/**
 * 日志拦截器
 *
 * @author Charles7c
 * @since 1.1.0
 */
@Component
@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    @Resource
    private  LogService logService;
    private final TransmittableThreadLocal<LogRecord.Started> timestampTtl = new TransmittableThreadLocal<>();


    @Override
    public boolean preHandle(@NonNull HttpServletRequest request,
                             @NonNull HttpServletResponse response,
                             @NonNull Object handler) {
        log.info("LogInterceptor");
        MDC.put(Constants.REQ_ID, IdUtil.getSnowflakeNextIdStr());
        Clock timestamp = Clock.systemUTC();
        if (this.isRequestRecord(handler, request)) {
            log.info("[{}] {}", request.getMethod(), request.getRequestURI());
            LogRecord.Started startedLogRecord = LogRecord.start(timestamp, new RecordableServletHttpRequest(request));
            timestampTtl.set(startedLogRecord);
        }
        return true;
    }

    @Override
    public void afterCompletion(@NonNull HttpServletRequest request,
                                @NonNull HttpServletResponse response,
                                @NonNull Object handler,
                                Exception e) {
        LogRecord.Started startedLogRecord = timestampTtl.get();
        if (null == startedLogRecord) {
            return;
        }
        timestampTtl.remove();
        try {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
            Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
            Set<IncludeEnum> includeSet = this.getIncludes(methodLog, classLog);
            LogRecord finishedLogRecord = startedLogRecord.finish(new RecordableServletHttpResponse(response, response
                    .getStatus()), includeSet);
            // 记录日志描述
            if (includeSet.contains(IncludeEnum.DESCRIPTION)) {
                this.logDescription(finishedLogRecord, methodLog, handlerMethod);
            }
            // 记录所属模块
            if (includeSet.contains(IncludeEnum.MODULE)) {
                this.logModule(finishedLogRecord, methodLog, classLog, handlerMethod);
            }
            LogResponse logResponse = finishedLogRecord.getResponse();
            log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), logResponse
                    .getStatus(), finishedLogRecord.getTimeTaken().toMillis());
            logService.save(finishedLogRecord);
        } catch (Exception ex) {
            log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
        }
        log.info("afterCompletion");
        MDC.clear();
    }

    /**
     * 获取日志包含信息
     *
     * @param methodLog 方法级 Log 注解
     * @param classLog  类级 Log 注解
     * @return 日志包含信息
     */
    private Set<IncludeEnum> getIncludes(Log methodLog, Log classLog) {
        Set<IncludeEnum> includeSet = IncludeEnum.defaultIncludes();
        if (null != classLog) {
            this.processInclude(includeSet, classLog);
        }
        if (null != methodLog) {
            this.processInclude(includeSet, methodLog);
        }
        return includeSet;
    }

    /**
     * 处理日志包含信息
     *
     * @param includes      日志包含信息
     * @param logAnnotation Log 注解
     */
    private void processInclude(Set<IncludeEnum> includes, Log logAnnotation) {
        IncludeEnum[] includeArr = logAnnotation.includes();
        if (includeArr.length > 0) {
            includes.addAll(Set.of(includeArr));
        }
        IncludeEnum[] excludeArr = logAnnotation.excludes();
        if (excludeArr.length > 0) {
            includes.removeAll(Set.of(excludeArr));
        }
    }

    /**
     * 记录描述
     *
     * @param logRecord     日志信息
     * @param methodLog     方法级 Log 注解
     * @param handlerMethod 处理器方法
     */
    private void logDescription(LogRecord logRecord, Log methodLog, HandlerMethod handlerMethod) {
        // 例如：@Log("新增部门") -> 新增部门
        if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.value())) {
            logRecord.setDescription(methodLog.value());
            return;
        }
        // 例如：@Operation(summary="新增部门") -> 新增部门
        ApiOperation methodOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
        if (null != methodOperation) {
            logRecord.setDescription(CharSequenceUtil.blankToDefault(methodOperation.value(), "请在该接口方法上指定日志描述"));
        }
    }

    /**
     * 记录模块
     *
     * @param logRecord     日志信息
     * @param methodLog     方法级 Log 注解
     * @param classLog      类级 Log 注解
     * @param handlerMethod 处理器方法
     */
    private void logModule(LogRecord logRecord, Log methodLog, Log classLog, HandlerMethod handlerMethod) {
        // 例如：@Log(module = "部门管理") -> 部门管理
        if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.module())) {
            logRecord.setModule(methodLog.module());
            return;
        }
        if (null != classLog && CharSequenceUtil.isNotBlank(classLog.module())) {
            logRecord.setModule(classLog.module());
            return;
        }
        // 例如：@Tag(name = "部门管理") -> 部门管理
        Api classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Api.class);
        if (null != classTag) {
            String name = classTag.tags()[0];
            logRecord.setModule(CharSequenceUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
        }
    }

    /**
     * 是否要记录日志
     *
     * @param handler 处理器
     * @param request 请求对象
     * @return true：需要记录；false：不需要记录
     */
    private boolean isRequestRecord(Object handler, HttpServletRequest request) {
        if (!(handler instanceof HandlerMethod )) {
            return false;
        }
        HandlerMethod handlerMethod= (HandlerMethod) handler;
        // 不拦截 /error
        ServerProperties serverProperties = SpringUtil.getBean(ServerProperties.class);
        if (request.getRequestURI().equals(serverProperties.getError().getPath())) {
            return false;
        }
        // 如果接口被隐藏，不记录日志
        ApiOperation methodOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
        if (null != methodOperation && methodOperation.hidden()) {
            return false;
        }
        Class<?> handlerBeanType = handlerMethod.getBeanType();
        // 如果接口方法或类上有 @Log 注解，且要求忽略该接口，则不记录日志
        Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
        if (null != methodLog && methodLog.ignore()) {
            return false;
        }
        Log classLog = handlerBeanType.getDeclaredAnnotation(Log.class);
        return null == classLog || !classLog.ignore();
    }
}
